<?php

namespace Common\Model;

use Think\Upload;

/**
 * 系统文件库
 * 存储标准：数据库中可以同一个文件存储多次，但本地文件系统只允许保存一个文件。
 * 因为每个用户对同一个文件都有不同地方的引用，并且文件也有归属的用户，所以允许不同用户多次存储同一个文件。
 * 本地文件被删除的条件：文件库中已经没有引用这个文件的数据，即可对服务器文件系统中的文件一并删除掉。
 * Class FileModel
 * @package Common\Model
 */
class FileModel extends BaseModel
{
    protected $_auto = array(
        array('create_time', NOW_TIME,),
    );
    protected $delete_list = array();
    protected $config = array();
    protected $num2text = [
        'size' => 'format_bytes',
        'create_time' => 'smart_time',
    ];
    protected $mimeMap = array(
        'application/envoy' => 'evy',
        'application/fractals' => 'fif',
        'application/futuresplash' => 'spl',
        'application/hta' => 'hta',
        'application/internet-property-stream' => 'acx',
        'application/mac-binhex40' => 'hqx',
        'application/msword' => 'dot',
        'application/octet-stream' => 'lzh',
        'application/oda' => 'oda',
        'application/olescript' => 'axs',
        'application/pdf' => 'pdf',
        'application/pics-rules' => 'prf',
        'application/pkcs10' => 'p10',
        'application/pkix-crl' => 'crl',
        'application/postscript' => 'ps',
        'application/rtf' => 'rtf',
        'application/set-payment-initiation' => 'setpay',
        'application/set-registration-initiation' => 'setreg',
        'application/vnd.ms-excel' => 'xlw',
        'application/vnd.ms-outlook' => 'msg',
        'application/vnd.ms-pkicertstore' => 'sst',
        'application/vnd.ms-pkiseccat' => 'cat',
        'application/vnd.ms-pkistl' => 'stl',
        'application/vnd.ms-powerpoint' => 'ppt',
        'application/vnd.ms-project' => 'mpp',
        'application/vnd.ms-works' => 'wps',
        'application/winhlp' => 'hlp',
        'application/x-bcpio' => 'bcpio',
        'application/x-cdf' => 'cdf',
        'application/x-compress' => 'z',
        'application/x-compressed' => 'tgz',
        'application/x-cpio' => 'cpio',
        'application/x-csh' => 'csh',
        'application/x-director' => 'dxr',
        'application/x-dvi' => 'dvi',
        'application/x-gtar' => 'gtar',
        'application/x-gzip' => 'gz',
        'application/x-hdf' => 'hdf',
        'application/x-internet-signup' => 'isp',
        'application/x-iphone' => 'iii',
        'application/x-javascript' => 'js',
        'application/x-latex' => 'latex',
        'application/x-msaccess' => 'mdb',
        'application/x-mscardfile' => 'crd',
        'application/x-msclip' => 'clp',
        'application/x-msdownload' => 'dll',
        'application/x-msmediaview' => 'mvb',
        'application/x-msmetafile' => 'wmf',
        'application/x-msmoney' => 'mny',
        'application/x-mspublisher' => 'pub',
        'application/x-msschedule' => 'scd',
        'application/x-msterminal' => 'trm',
        'application/x-mswrite' => 'wri',
        'application/x-netcdf' => 'nc',
        'application/x-perfmon' => 'pmw',
        'application/x-pkcs12' => 'pfx',
        'application/x-pkcs7-certificates' => 'spc',
        'application/x-pkcs7-certreqresp' => 'p7r',
        'application/x-pkcs7-mime' => 'p7m',
        'application/x-pkcs7-signature' => 'p7s',
        'application/x-sh' => 'sh',
        'application/x-shar' => 'shar',
        'application/x-shockwave-flash' => 'swf',
        'application/x-stuffit' => 'sit',
        'application/x-sv4cpio' => 'sv4cpio',
        'application/x-sv4crc' => 'sv4crc',
        'application/x-tar' => 'tar',
        'application/x-tcl' => 'tcl',
        'application/x-tex' => 'tex',
        'application/x-texinfo' => 'texinfo',
        'application/x-troff' => 'tr',
        'application/x-troff-man' => 'man',
        'application/x-troff-me' => 'me',
        'application/x-troff-ms' => 'ms',
        'application/x-ustar' => 'ustar',
        'application/x-wais-source' => 'src',
        'application/x-x509-ca-cert' => 'der',
        'application/ynd.ms-pkipko' => 'pko',
        'application/zip' => 'zip',
        'audio/basic' => 'snd',
        'audio/mid' => 'rmi',
        'audio/mpeg' => 'mp3',
        'audio/x-aiff' => 'aiff',
        'audio/x-mpegurl' => 'm3u',
        'audio/x-pn-realaudio' => 'ram',
        'audio/x-wav' => 'wav',
        'image/bmp' => 'bmp',
        'image/cis-cod' => 'cod',
        'image/gif' => 'gif',
        'image/ief' => 'ief',
        'image/jpeg' => 'jpg',
        'image/pipeg' => 'jfif',
        'image/svg+xml' => 'svg',
        'image/tiff' => 'tiff',
        'image/x-cmu-raster' => 'ras',
        'image/x-cmx' => 'cmx',
        'image/x-icon' => 'ico',
        'image/x-portable-anymap' => 'pnm',
        'image/x-portable-bitmap' => 'pbm',
        'image/x-portable-graymap' => 'pgm',
        'image/x-portable-pixmap' => 'ppm',
        'image/x-rgb' => 'rgb',
        'image/x-xbitmap' => 'xbm',
        'image/x-xpixmap' => 'xpm',
        'image/x-xwindowdump' => 'xwd',
        'message/rfc822' => 'nws',
        'text/css' => 'css',
        'text/h323' => '323',
        'text/html' => 'stm',
        'text/iuls' => 'uls',
        'text/plain' => 'txt',
        'text/richtext' => 'rtx',
        'text/scriptlet' => 'sct',
        'text/tab-separated-values' => 'tsv',
        'text/webviewhtml' => 'htt',
        'text/x-component' => 'htc',
        'text/x-setext' => 'etx',
        'text/x-vcard' => 'vcf',
        'video/mpeg' => 'mpv2',
        'video/quicktime' => 'qt',
        'video/x-la-asf' => 'lsx',
        'video/x-ms-asf' => 'asx',
        'video/x-msvideo' => 'avi',
        'video/x-sgi-movie' => 'movie',
        'x-world/x-vrml' => 'xof',
    );

    public function config($config)
    {
        $this->config = $config;
        return $this;
    }

    public function upload($userId, $from = 'normal', $driver = 'Local', $driverConfig = array())
    {
        $config = $this->config;
        $this->config = array(); // 重置配置
        $config['callback'] = array($this, 'hashCheck');
        $config['removeTrash'] = array($this, 'removeTrash');
        $uploader = new Upload($config, $driver, $driverConfig);
        $info = $uploader->upload();
        if ($info) {
            foreach ($info as $key => &$row) {
                // 无论怎么样 每一次上传都会向数据库记录一次 但本地文件只保存一次
                $data = $this->create($row);
                $data = $data ?: array();
                unset($data['id']);
                $data['userId'] = $userId;
                $data['location'] = $driver;
                $data['from'] = $from;
                $data['rootpath'] = $config['rootPath'];
                if ($row['type']) {
                    $data['mime'] = $row['type'];
                }
//                $data['mime'] = $row[''];
                /* 记录文件信息 */
                if ($id = $this->add($data)) {
                    $row['id'] = $id;
                } else {
                    //TODO: 文件上传成功，但是记录文件信息失败，需记录日志
                    unset($info[$key]);
                }
            }
            return $info;
        } else {
            $this->error = $uploader->getError();
            return false;
        }
    }

    /**
     * 上传base64内容的文件
     * @param $userId
     * @param $data
     * @param $file
     * @param string $from
     */
    public function upImgByBase64($userId, $base64Data, $file = array(), $from = 'normal')
    {
        $base64Data = base64_decode($base64Data);
        $size = strlen($base64Data);
        // 存入临时文件 保证系统安全
        $tmpfile = tmpfile();
        $meta = stream_get_meta_data($tmpfile); // eg: /tmp/phpFx0513a
        fwrite($tmpfile, $base64Data);
        $info = getimagesize($meta['uri']);
        $file['md5'] = md5_file($meta['uri']);
        $file['sha1'] = sha1_file($meta['uri']);
        fclose($tmpfile);// 关闭临时文件
        if (!$info) {
            $this->error = '只能上传图片';
            return false;
        } else if (!$file['name']) {
            $this->error = '请设置文件名';
            return false;
        } else if ($size > $this->config['maxSize']) {
            $this->error = '文件大小超出限制';
            return false;
        }

        $file['ext'] = pathinfo($file['name'], PATHINFO_EXTENSION);
        $file['size'] = $size;
        $file['mime'] = $info['mime'];
        $file['rootpath'] = $this->config['rootPath'];
        $file['savename'] or $file['savename'] = uniqid() . '.' . $file['ext'];
        $file['savepath'] or $file['savepath'] = date('Y-m-d', NOW_TIME) . '/';
        if ($row = $this->where(array('md5' => $file['md5'], 'sha1' => $file['sha1']))->field(true)->find()) {
            unset($row['name']);
            $file = $row;
        } else {
            $dir = $file['rootpath'] . $file['savepath'];
            if (!$this->mkdir($dir)) return false; // 存储目录创建失败
            $path = $dir . $file['savename'];
            if (!file_put_contents($path, $base64Data)) {
                $this->error = '文件保存失败';
                return false;
            }
        }
        // 文件独立的属性
        $file['userId'] = $userId;
        $file['create_time'] = NOW_TIME;
        $file['from'] = $from;
        $file['location'] = 'Local';
        $file['id'] = $this->add($file);
        if (!$file['id']) return false;
        return $file;
    }

    /**
     * 创建目录
     * @param  string $path 要创建的路径
     * @return boolean          创建状态，true-成功，false-失败
     */
    public function mkdir($path)
    {
        if (is_dir($path)) {
            return true;
        }
        if (mkdir($path, 0777, true)) {
            return true;
        } else {
            $this->error = "目录 {$path} 创建失败！";
            return false;
        }
    }

    /**
     * 下载文件库中的文件
     * @param  integer $id 文件ID
     * @param  string $args 回调函数参数
     * @return boolean       false-下载失败，否则输出下载文件
     */
    public function download($id, $callback = null, $args = null)
    {
        /* 获取下载文件信息 */
        $file = $this->find($id);
        if (!$file) {
            $this->error = '不存在该文件！';
            return false;
        }

        /* 下载文件 */
        switch ($file['location']) {
            case 0: //下载本地文件
                return $this->downLocalFile($file, $callback, $args);
            case 1: //TODO: 下载远程FTP文件
                break;
            default:
                $this->error = '不支持的文件存储类型！';
                return false;

        }

    }

    /**
     * 上传时如果数据库有数据 而本地文件不存在文件 执行该方法删除数据库中的该条垃圾数据
     * @param $data
     */
    public function removeTrash($data)
    {
        $this->where(array('id' => $data['id'],))->delete();
    }

    /**
     * 下载本地文件
     * @param  array $file 文件信息数组
     * @param  callable $callback 下载回调函数，一般用于增加下载次数
     * @param  string $args 回调函数参数
     * @return boolean            下载失败返回false
     */
    public function downLocalFile($file, $callback = null, $args = null)
    {
        if (is_file($file['rootpath'] . $file['savepath'] . $file['savename'])) {
            /* 调用回调函数新增下载数 */
            is_callable($callback) && call_user_func($callback, $args);

            /* 执行下载 */ //TODO: 大文件断点续传
            header("Content-Description: File Transfer");
            header('Content-type: ' . $file['mime']);
            header('Content-Length:' . $file['size']);
            if (preg_match('/MSIE/', $_SERVER['HTTP_USER_AGENT'])) { //for IE
                header('Content-Disposition: attachment; filename="' . rawurlencode($file['name']) . '"');
            } else {
                header('Content-Disposition: attachment; filename="' . $file['name'] . '"');
            }
            readfile($file['rootpath'] . $file['savepath'] . $file['savename']);
            exit;
        } else {
            $this->error = '文件已被删除！';
            return false;
        }
    }

    /**
     * 文件哈希检测
     * 保证文件只保存一次
     * @param $file
     * @return mixed
     * @throws \Exception
     */
    public function hashCheck($file)
    {
        if (empty($file['md5'])) {
            throw new \Exception('缺少参数:md5');
        }
        /* 查找文件 */
        $map = array('md5' => $file['md5'], 'sha1' => $file['sha1'],);
        $res = $this->field(true)->where($map)->order('id desc')->find();
        if ($res) {
            $res['path'] = ltrim($res['rootpath'], '.') . '/' . $res['savepath'] . '/' . $res['savename']; // 响应upload类
        }
        return $res;
    }

    // 文件删除
    protected function _before_delete($options)
    {
        parent::_before_delete($options);
        if ($options['where']) {
            $this->delete_list = $this->where($options['where'])->field(true)->select(); // 先记录被删除的数据
        }
    }

    // 删除后置
    public function _after_delete($data, $options)
    {
        parent::_after_delete($data, $options);
        if ($this->delete_list) {
            // 如果删除成功则开始删除本地文件
            foreach ($this->delete_list as $item) {
                $map = array(
                    'rootpath' => $item['rootpath'],
                    'savepath' => $item['savepath'],
                    'savename' => $item['savename'],
                );
                $had = $this->where($map)->count();
                if ($item['location'] == 'Local' && !$had) {
                    // 当为本地文件 且已经没有引用到该文件的数据时 删除本地文件
                    $path = $item['rootpath'] . '/' . $item['savepath'] . '/' . $item['savename'];
                    if (is_file($path)) unlink($path); // 执行删除
                }
            }
            $this->delete_list = array(); // 释放
        }
    }

    public function _after_find(&$result, $options)
    {
        parent::_after_find($result, $options);
        if ($result['id'] && $result['rootpath'] && $result['savepath'] && $result['savename']) {
            $result['download_url'] = U2Encrypt('Api/Download/index', ['aid' => $result['id']], 'main_url');
        }
    }
}
